package net.sourceforge.stripes.tag;

import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.BodyTag;

import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.ajax.JavaScriptBuilder;
import net.sourceforge.stripes.controller.ParameterName;
import net.sourceforge.stripes.controller.StripesFilter;
import net.sourceforge.stripes.exception.StripesJspException;
import net.sourceforge.stripes.localization.LocalizationUtility;
import net.sourceforge.stripes.util.Log;
import net.sourceforge.stripes.util.bean.PropertyExpression;
import net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation;
import net.sourceforge.stripes.validation.ValidationMetadata;
import net.sourceforge.stripes.validation.ValidationMetadataProvider;

/**
 * <p>Field metadata tag for use with the Stripes framework. Exposes field properties via JavaScript to
 * allow client side validation. If this tag has a body it will be wrapped with JavaScript tags for
 * convenience.</p>
 * 
 * @author Aaron Porter
 * 
 */
public class FieldMetadataTag extends HtmlTagSupport implements BodyTag {
    /** Log used to log error and debugging information for this class. */
    private static final Log log = Log.getInstance(FormTag.class);

    /** Name of variable to hold metadata. */
    private String var;
    /** Optional comma separated list of additional fields to expose. */
    private String fields;
    /** Set to true to include type information for all fields. */
    private boolean includeType = false;
    /** Set to true to include the fully qualified class name for all fields. */
    private boolean fqn = false;
    /** Stores the value of the action attribute before the context gets appended. */
    private String actionWithoutContext;
    
    public FormTag getForm() {
        return getParentTag(FormTag.class);
    }

    /**
     * Builds a string that contains field metadata in a JavaScript object.
     * 
     * @return JavaScript object containing field metadata
     */
    private String getMetadata() {
        ActionBean bean = null;

        String action = getAction();

        FormTag form = getForm();

        if (form != null) {
            if (action != null)
                log.warn("Parameters action and/or beanclass specified but field-metadata tag is inside of a Stripes form tag. The bean will be pulled from the form tag.");
            
            action = form.getAction();
        }

        if (form != null)
            bean = form.getActionBean();

        Class<? extends ActionBean> beanClass = null;

        if (bean != null)
            beanClass = bean.getClass();
        else if (action != null) {
            beanClass = StripesFilter.getConfiguration().getActionResolver().getActionBeanType(action);
            if (beanClass != null) {
                try {
                    bean = beanClass.newInstance();
                }
                catch (Exception e) {
                    log.error(e);
                    return null;
                }
            }
        }

        if (beanClass == null) {
            log.error("Couldn't determine ActionBean class from FormTag! One of the following conditions must be met:\r\n\t",
                        "1. Include this tag inside of a stripes:form tag\r\n\t",
                        "2. Use the action parameter\r\n\t",
                        "3. Use the beanclass parameter");
            return null;
        }

        ValidationMetadataProvider metadataProvider = StripesFilter.getConfiguration()
                .getValidationMetadataProvider();

        if (metadataProvider == null) {
            log.error("Couldn't get ValidationMetadataProvider!");
            return null;
        }

        Map<String, ValidationMetadata> metadata = metadataProvider
                .getValidationMetadata(beanClass);

        StringBuilder sb = new StringBuilder("{\r\n\t\t");

        Set<String> fields = new HashSet<String>();
        
        if (form != null) {
            for (String field : form.getRegisteredFields()) {
                fields.add(new ParameterName(field).getStrippedName());
            }
        }

        if ((this.fields != null) && (this.fields.trim().length() > 0))
            fields.addAll(Arrays.asList(this.fields.split(",")));
        else if (form == null) {
            log.error("Fields attribute is required when field-metadata tag isn't inside of a Stripes form tag.");
            return null;
        }

        boolean first = true;
        
        Locale locale = getPageContext().getRequest().getLocale();

        for (String field : fields) {

            PropertyExpressionEvaluation eval = null;
            
            try {
                eval = new PropertyExpressionEvaluation(PropertyExpression.getExpression(field), bean);
            }
            catch (Exception e) {
                continue;
            }

            Class<?> fieldType = eval.getType();

            ValidationMetadata data = metadata.get(field);

            StringBuilder fieldInfo = new StringBuilder();

            if (fieldType.isPrimitive() || Number.class.isAssignableFrom(fieldType)
                    || Date.class.isAssignableFrom(fieldType) || includeType) {
                fieldInfo.append("type:").append(
                        JavaScriptBuilder.quote(fqn ? fieldType.getName() : fieldType
                                .getSimpleName()));
            }
            
            Class<?> typeConverterClass = null;
            
            if (data != null) {
                if (data.encrypted())
                    fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("encrypted:")
                            .append(data.encrypted());
                if (data.required())
                    fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("required:").append(
                            data.required());
                if (data.on() != null) {
                    fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("on:[");
                    Iterator<String> it = data.on().iterator();
                    while (it.hasNext()) {
                        fieldInfo.append(JavaScriptBuilder.quote(it.next()));
                        if (it.hasNext())
                            fieldInfo.append(",");
                    }
                    fieldInfo.append("]");
                }
                if (data.trim())
                    fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("trim:").append(
                            data.trim());
                if (data.mask() != null)
                    fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("mask:")
                            .append("new RegExp(")
                            .append(JavaScriptBuilder.quote("^" + data.mask().toString() + "$"))
                            .append(")");
                if (data.minlength() != null)
                    fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("minlength:")
                            .append(data.minlength());
                if (data.maxlength() != null)
                    fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("maxlength:")
                            .append(data.maxlength());
                if (data.minvalue() != null)
                    fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("minvalue:").append(
                            data.minvalue());
                if (data.maxvalue() != null)
                    fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("maxvalue:").append(
                            data.maxvalue());
                
                String label = data.label();
                if (data.label() == null)
                {
                    label = LocalizationUtility.getLocalizedFieldName(field,
                            form == null ? null : form.getAction(),
                            form == null ? null : form.getActionBeanClass(),
                            locale);
                }
                if (label != null)
                    fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("label:").append(
                            JavaScriptBuilder.quote(label));
                
                typeConverterClass = data.converter();
            }

            // If we couldn't get the converter from the validation annotation
            // try to get it from the TypeConverterFactory
            if (typeConverterClass == null) {
                try {
                    typeConverterClass = StripesFilter.getConfiguration().getTypeConverterFactory()
                            .getTypeConverter(fieldType, pageContext.getRequest().getLocale())
                            .getClass();
                }
                catch (Exception e) {
                    // Just ignore it
                }
            }

            if (typeConverterClass != null) {
                fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("typeConverter:")
                        .append(
                                JavaScriptBuilder.quote(fqn ? typeConverterClass.getName()
                                        : typeConverterClass.getSimpleName()));
            }


            if (fieldInfo.length() > 0) {
                if (first)
                    first = false;
                else
                    sb.append(",\r\n\t\t");

                sb.append(JavaScriptBuilder.quote(field)).append(":{");

                sb.append(fieldInfo);

                sb.append("}");
            }
        }

        sb.append("\r\n\t}");

        return sb.toString();
    }

    public FieldMetadataTag() {
        getAttributes().put("type", "text/javascript");
    }

    public void doInitBody() throws JspException {
    }

    public int doAfterBody() throws JspException {
        return SKIP_BODY;
    }

    @Override
    public int doStartTag() throws JspException {
        getPageContext().setAttribute(getVar(), new Var(getMetadata()));
        return EVAL_BODY_BUFFERED;
    }

    @Override
    public int doEndTag() throws JspException {
        JspWriter writer = getPageContext().getOut();

        String body = getBodyContentAsString();

        if (body != null) {
            try {
                String contentType = getPageContext().getResponse().getContentType();
                
                // Catches application/x-javascript, text/javascript, and text/ecmascript
                boolean pageIsScript = contentType != null && contentType.toLowerCase().contains("ascript");
                
                // Don't write the script tags if this page is a script
                if (!pageIsScript) {
                    writeOpenTag(writer, "script");
                    writer.write("//<![CDATA[\r\n");
                }

                writer.write(body);

                if (!pageIsScript) {
                    writer.write("\r\n//]]>");
                    writeCloseTag(writer, "script");
                }
            }
            catch (IOException ioe) {
                throw new StripesJspException("IOException while writing output in LinkTag.", ioe);
            }
        }
        
        // Only keep the type attribute between uses
        String type = getAttributes().get("type");
        getAttributes().clear();
        getAttributes().put("type", type);

        return SKIP_BODY;
    }

    public String getVar() {
        return var;
    }

    /**
     * Sets the name of the variable to hold metadata.
     * 
     * @param var the name of the attribute that will contain field metadata
     */
    public void setVar(String var) {
        this.var = var;
    }

    public String getFields() {
        return fields;
    }

    /**
     * Optional comma separated list of additional fields to expose. Any fields that have
     * already been added to the Stripes form tag will automatically be included.
     * 
     * @param fields comma separated list of field names
     */
    public void setFields(String fields) {
        this.fields = fields;
    }

    public boolean isIncludeType() {
        return includeType;
    }

    /**
     * Set to true to include type information for all fields. By default, type information is only
     * included for primitives, numbers, and dates.
     * 
     * @param includeType include type info for all fields
     */
    public void setIncludeType(boolean includeType) {
        this.includeType = includeType;
    }

    public boolean isFqn() {
        return fqn;
    }

    /**
     * Set to true to include the fully qualified class name for all fields.
     * 
     * @param fqn include fully qualified class name for all fields
     */
    public void setFqn(boolean fqn) {
        this.fqn = fqn;
    }

    /**
     * Sets the action for the form. If the form action begins with a slash, and does not already
     * contain the context path, then the context path of the web application will get prepended to
     * the action before it is set. In general actions should be specified as &quot;absolute&quot;
     * paths within the web application, therefore allowing them to function correctly regardless of
     * the address currently shown in the browser&apos;s address bar.
     * 
     * @param action the action path, relative to the root of the web application
     */
    public void setAction(String action) {
        // Use the action resolver to figure out what the appropriate URL binding if for
        // this path and use that if there is one, otherwise just use the action passed in
        String binding = StripesFilter.getConfiguration().getActionResolver()
                .getUrlBindingFromPath(action);
        if (binding != null) {
            this.actionWithoutContext = binding;
        }
        else {
            this.actionWithoutContext = action;
        }
    }

    public String getAction() {
        return this.actionWithoutContext;
    }

    /**
     * Sets the 'action' attribute by inspecting the bean class provided and asking the current
     * ActionResolver what the appropriate URL is.
     * 
     * @param beanclass the String FQN of the class, or a Class representing the class
     * @throws StripesJspException if the URL cannot be determined for any reason, most likely
     *             because of a mis-spelled class name, or a class that's not an ActionBean
     */
    public void setBeanclass(Object beanclass) throws StripesJspException {
        String url = getActionBeanUrl(beanclass);
        if (url == null) {
            throw new StripesJspException(
                    "Could not determine action from 'beanclass' supplied. "
                            + "The value supplied was '"
                            + beanclass
                            + "'. Please ensure that this bean type "
                            + "exists and is in the classpath. If you are developing a page and the ActionBean "
                            + "does not yet exist, consider using the 'action' attribute instead for now.");
        }
        else {
            setAction(url);
        }
    }

    /** Corresponding getter for 'beanclass', will always return null. */
    public Object getBeanclass() {
        return null;
    }
    /**
     * This is what is placed into the request attribute. It allows us to
     * get the field metadata as well as the form id.
     */
    public class Var {
        private String fieldMetadata, formId;

        private Var(String fieldMetadata) {
            this.fieldMetadata = fieldMetadata;
            FormTag form = getForm();
            if (form != null) {
                if (form.getId() == null)
                    form.setId("stripes-" + new Random().nextInt());
                this.formId = form.getId();
            }
        }

        @Override
        public String toString() {
            return fieldMetadata;
        }

        public String getFormId() {
            return formId;
        }
    }
}
